<?php

require_once 'common.php';



/**
 * Table
 * 
 * Something that stores records.
 * 
 * Records are identified by key field (which is necessary).
 * Options for fields: index(boolean), timestamp(boolean), length(int)
 * Index on some field is necessary for selecting records by this field.
 * 
 * No sorting by index is implemented (yet?)
 * Selecting is very limited (see the comment for $this->Select() method) 
 * 
 * @author Kirill Krasnoschekov
 */
abstract class Table
{
    // Table name
    protected $name;
    
    // Dictionary "field_name" => array of parameters
    protected $schema;
    
    // Links to index fields
    protected $indexes;
    
    // Some error messages
    protected static $err = array(
        'SCHEMA_FIELDNAME_DOUBLING' => "Field name doubling!",
        'SCHEMA_FIELDNAME_INCORRECT' => "Incorrect field name!",
        'INSERT_NO_VALUE' => "No value is specified for some fields!"
        );
    
    
    
    /**
     * Constructor
     * 
     * Creates database fields list (with parameters) and indexes list
     * 
     * @param array $schema
     * Array (FieldName1 => array_of_parameters, 
     *        ... , 
     *        FieldNameN => array_of_parameters)
     * @param string $name Name of the table (may be used in filenames)
     * @author Kirill Krasnoschekov
     */
    public function __construct(array $schema, $name)
    {
        trace("<p>Creating table '{$name}'...</p>");

        // Assign the name
        $this->name = $name;
        
        // Create primary key field
        trace("Adding key field 'id'...");
        $this->schema["id"] = array();
        $this->schema["id"]["key"] = True;
        $this->schema["id"]["index"] = True;
        
        // Add it to index fields list
        $this->indexes["id"] = $this->schema["id"];
        
        // Create other fields
        // 'f' in f<VarName> stands for 'field'
        // 'p' in p<VarName> - for 'parameter'
        foreach ( $schema as $fName => $fParam )    
        {
            trace("Adding field '{$fName}'...");
            
            // Error if name is incorrect
            if ( !is_string($fName) )
            {
                die(self::$err['SCHEMA_FIELDNAME_INCORRECT']);
                continue;
            } 
            
            // Error if field name is already occupied
            if ( isset($this->schema[$fName]) )
            {
                die(self::$err['SCHEMA_FIELDNAME_DOUBLING']);
                continue;
            }
            
            // Create the field
            $this->schema[$fName] = array();
            
            // Assign parameters
            // Skip this part if argument is incorrect
            if ( !is_array($fParam) )
            {
                continue;
            }
            
            foreach ( $fParam as $pName => $pValue )    
            {
                trace("Assigning parameters...");
                
                // Is given parameter available?
                switch ( $pName ) {
                    case 'index':
                        // Make link from fields array to index array
                        $this->indexes[$fName] = $this->schema[$fName];
                        break;
                        
                    case 'timestamp':
                        $this->schema[$fName][$pName] = True;
                        break;

                    case 'length':
                        if ( is_int($pValue) )
                        {
                            $this->schema[$fName][$pName] = $pValue;
                        }
                        break;
                }
            }
        }
        
        traceVar($this->schema, "Schema of table {$this->name}");
    }

    /**
     * Destructor (empty)
     * 
     * @author Kirill Krasnoschekov
     */
    public function __destruct() {}
    
    
    
    /**
     * Insert data into table 
     * 
     * Top-level function that only checks data 
     * and calls low-level function which actually writes the row. 
     * 
     * @param array $data Record in the dictionary form
     * array( FieldName1e => value1 , ... , FieldNameN => valueN)
     * @author Kirill Krasnoschekov
     */
    public function Insert(array $data)
    {
        trace("<p>Entering Insert function...</p>");
        
        // Preparing data
        $preparedData = $this->PrepareData($data);
        
        traceVar($preparedData, "Data to insert:");
        
        // Actually writing the data
        return $this->_Insert($preparedData);
    }
    
    /**
     * Delete the record with given id.
     * 
     * @param int $id
     * @author Kirill Krasnoschekov
     */
    public function Delete($id)
    {
        // TODO Perform delete checks
        
        return $this->_Delete($id);
    }
    
    /**
     * Update the record with id $id by data $data.
     * 
     * @param int $id
     * @param array $data New data in the dictionary form:
     * array( FieldName1e => value1 , ... , FieldNameN => valueN)
     * @author Kirill Krasnoschekov
     */
    public function Update($id, array $data)
    {
        // Deleting the old record
        $this->_Delete($id);
        
        // Building data for the new record (using old id!)
        $data['id'] = $id;
        $preparedData = $this->PrepareData($data);
        
        // Writing
        return $this->_Insert($preparedData);
    }

    /**
     * Select records from the table
     * 
     * Provides some minimalistic functionality for fetching data
     * from the table.
     * 
     * @param array $cond Array of conditions: 
     * (field1 => value1, ... , fieldN => valueN)
     * which means
     * (field1 == value1 AND ... AND fieldN == valueN)
     * @param int $start Number of the record to start from
     * @param int $end Number of the record to finish with
     * @param bool $inv Inverse records order?
     * @return array of records
     * @author Kirill Krasnoschekov
     */
    public function Select(array $cond, $start = 0, $end = 0, $inv = False)
    {
        traceVar($cond, "Query conditions:");
        
        // TODO Perform select checks
        
        return $this->_Select($cond, $start, $end, $inv);
    }

    
    
    /**
     * Prepares data for insertion.
     * 
     * Makes checks and auto-generates values if needed.
     * Priority: 
     * 1. data in $data array
     * 2. auto-generated values for field types
     * 
     * @param array $data
     * @return array Prepared data
     * @author Kirill Krasnoschekov
     */
    private function PrepareData($data)
    {
        trace("<p>Preparing data for insertion...</p>");
        
        $preparedData = array();
        foreach ( $this->schema as $fName => $fParam )
        {
            trace("Field '{$fName}'...");
            
            // Is given by user?
            if ( isset($data[$fName]) )
            {
                trace("User value given.");
                // TODO Check length (and type?)
                $preparedData[$fName] = $data[$fName];
                continue;
            }
            
            // Try to generate
            if ( $value = $this->GenerateValue($fName) )
            {
                trace("Trying to auto-generate.");
                $preparedData[$fName] = $value;
                continue;
            }

            // Can't get data for this field:
            // error
            die(self::$err['INSERT_NO_VALUE']);
        }
        
        return $preparedData;
    }

    /**
     * Automatically build value for the field if it's available.
     * 
     * Available fields: (primary) key, timestamp
     * 
     * @param string $fName Field name
     * @return generated value or False
     * @author Kirill Krasnoschekov
     */
    private function GenerateValue($fName)
    {
        trace("Generating value for field '{$fName}'...");
        
        // Get field parameters
        $fParam = $this->schema[$fName];
        
        // Is auto-value available for this field?
        if ( array_key_exists('key', $fParam) ? $fParam['key'] : False)
        {
            // Key
            return $this->GetNextKey();
        } 
        elseif ( array_key_exists('timestamp', $fParam) )
        {
            // Timestamp
            return time();
        }
        
        // No auto-value found
        return False;
    }



    /*
     * 
     * Abstract functions
     * 
     */

    /**
     * Does the record with given id exist?
     * 
     * @param $id
     * @return True if does, False otherwise
     */
    public abstract function Exists($id);
    
    /**
     * Count rows in table
     */
    public abstract function CountRows();
    
    
    
    /*
     * Functions that actually change the storage
     */
     
    protected abstract function _Insert(array $data);
    protected abstract function _Delete($id);
    protected abstract function _Select(array $cond, $start, $end, $inv);
    
    /**
     * Find next key value which can be used for the new record
     * 
     * @return int Next id
     */
    protected abstract function GetNextKey();
}

?>